﻿using System;
using System.Collections.Generic;
using System.Threading;

namespace TypeBuilderFx.Core
{
    /// <summary>Base class for implementations of <see cref="ITypeEmitterCache"/>.</summary>
    public abstract class TypeEmitterCacheBase : ITypeEmitterCache
    {
        #region Inner Types
        private class TypeEmitterKey
        {
            private readonly Type _typeEmitterType;
            private readonly object _options;

            private TypeEmitterKey(Type typeEmitterType, object options)
            {
                _typeEmitterType = typeEmitterType;
                _options = options;
            }

            public static TypeEmitterKey Create<T_OPTIONS>(TypeEmitterBase<T_OPTIONS> typeEmitter)
                where T_OPTIONS : TypeEmitterOptions
            {
                if (typeEmitter == null)
                {
                    throw new ArgumentNullException("typeEmitter");
                }

                return new TypeEmitterKey(typeEmitter.GetType(), typeEmitter.Options);
            }

            #region object Methods
            public override bool Equals(object obj)
            {
                TypeEmitterKey key = obj as TypeEmitterKey;

                return key != null
                    && _typeEmitterType.Equals(key._typeEmitterType)
                    && _options.Equals(key._options);
            }

            public override int GetHashCode()
            {
                return _typeEmitterType.GetHashCode() ^ _options.GetHashCode();
            }
            #endregion
        }
        #endregion

        private readonly object _syncObject = new object();
        private IDictionary<TypeEmitterKey, object> _factoryCache = new Dictionary<TypeEmitterKey, object>();
        private int _typeCounter = 0;
        private bool _isDisposed = false;

        /// <summary>Destructor.</summary>
        ~TypeEmitterCacheBase()
        {
            Dispose(false);
        }

        #region IDisposable Members
        void IDisposable.Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion

        #region ITypeEmitterCache Members
        object ITypeEmitterCache.GetFactory<T_OPTIONS>(TypeEmitterBase<T_OPTIONS> typeEmitter)
        {
            object cachedValue = GetUntypedFactory(typeEmitter);

            return cachedValue;
        }
        #endregion

        /// <summary>Called only when the component needs to disposed (therefore only once).</summary>
        protected abstract void OnDispose();

        /// <summary>Returns a module builder.</summary>
        protected abstract ModuleBuilderExtender ModuleBuilder { get; }

        /// <summary>Returns a unique string based on a unique-ID but formatted in a way it can be used in a type name.</summary>
        /// <returns></returns>
        protected static string GetUniqueString()
        {
            return Guid.NewGuid().ToString("N");
        }

        private void Dispose(bool disposing)
        {
            try
            {
                if (disposing && !_isDisposed)
                {
                    OnDispose();
                }
            }
            finally
            {
                _isDisposed = true;
            }
        }

        private object GetUntypedFactory<T_OPTIONS>(TypeEmitterBase<T_OPTIONS> typeEmitter)
            where T_OPTIONS : TypeEmitterOptions
        {   //  This method is separate since it doesn't use generic, in order to avoid JIT bloat
            if (typeEmitter == null)
            {
                throw new ArgumentNullException("typeEmitter");
            }

            TypeEmitterKey key = TypeEmitterKey.Create(typeEmitter);
            object cachedValue = null;

            if (!_factoryCache.TryGetValue(key, out cachedValue))
            {   //  We lock the cache for the duration of the type emission
                lock (_syncObject)
                {
                    if (!_factoryCache.TryGetValue(key, out cachedValue))
                    {   //  The cache dictionaries are always readonly, so we recreate a new one,
                        //  add the item and switch the cache atomically
                        IDictionary<TypeEmitterKey, object> newFactoryCache =
                            new Dictionary<TypeEmitterKey, object>(_factoryCache);

                        //  Create the new factory
                        cachedValue = CreateFactory(typeEmitter);
                        //  Add it to the new cache and switch the caches
                        newFactoryCache.Add(key, cachedValue);
                        Interlocked.Exchange(ref _factoryCache, newFactoryCache);
                    }
                }
            }

            return cachedValue;
        }

        private object CreateFactory<T_OPTIONS>(TypeEmitterBase<T_OPTIONS> typeEmitter)
            where T_OPTIONS : TypeEmitterOptions
        {
            int currentCounter = Interlocked.Increment(ref _typeCounter);
            string uniqueString = GetUniqueString();
            string componentTypeName =
                string.IsNullOrEmpty(typeEmitter.Options.EmittedTypeName)
                ? string.Concat('C', currentCounter, '_', typeEmitter.GetType().Name, '_', uniqueString)
                : typeEmitter.Options.EmittedTypeName;
            string factoryTypeName =
                string.IsNullOrEmpty(typeEmitter.Options.EmittedFactoryTypeName)
                ? string.Concat('F', currentCounter, '_', typeEmitter.GetType().Name, '_', uniqueString)
                : typeEmitter.Options.EmittedFactoryTypeName;
            object factory = typeEmitter.EmitFactory(componentTypeName, factoryTypeName, ModuleBuilder);

            if (factory == null)
            {
                throw new ApplicationException("Type emitter returned a null factory");
            }

            return factory;
        }

        private static string GetFactoryInterfaceName(Type factoryType)
        {
            if (factoryType.GetInterfaces().Length == 1)
            {
                Type interfaceType = factoryType.GetInterfaces()[0];

                return interfaceType.FullName;
            }

            return "{Can't find the interface type}";
        }
    }
}